"""
    财务因子检验
"""
import datetime
import os
import warnings
from pathlib import Path

import pandas as pd
import numpy as np
from data_resource.data_bases import engine
from utilities.utilities_func import get_next_tradeDate, get_price
import matplotlib.pyplot as plt


class FinancialFactor:
    """
    财务因子检验思路：
    按报告期结束日对截面因子进行排序，持有至下一报告期法定发布日期
    - 年报: 4/30, 12/31
    - 1Q: 4/30, 3/31
    - 2Q: 8/31, 6/30
    - 3Q: 10/31, 930
    -------------------------------------------------
    频率太低，直接使用分组回测
    """

    def __init__(self, factor: pd.DataFrame, groupN: int = 5):
        """
        :params factor: 财务因子数据，columns=['code', 'end_date', 'f_ann_date', 'signal']
        :params tradingdate: 财务截面的下一交易日，columns=['calculate_temp', 'next_date']
        """
        self.factor = factor
        self.factor['end_date'] = pd.to_datetime(factor['end_date']).dt.date
        self.factor['f_ann_date'] = pd.to_datetime(factor['f_ann_date']).dt.date
        self.group = groupN
        # 获取交易日序列
        _sql1 = """
            select cal_date, is_open, pretrade_date from quant_research.basic_trading_date
            where is_open=1
        """
        self.trade_date = pd.read_sql(_sql1, engine)
        self.trade_date['cal_date'] = pd.to_datetime(self.trade_date['cal_date']).dt.date

        # 结果参数
        self.holding = None
        self.group_return = None

        # IC值
        self.ic = None

    def group_report(self, signal_name='signal'):
        """
        分组检测，最后一个报告期仅计算
        默认：等权计算
        """
        result = []
        self.factor.rename(columns={signal_name: 'signal'}, inplace=True)

        for date in self.factor['end_date'].unique():
            # 直接忽略了年报数据
            if date.month == 3:
                _temp = datetime.date(date.year, 4, 30)
                _signal = self.factor[(self.factor['end_date'] == date) & (self.factor['f_ann_date'] <= _temp)].copy()
                _signal['calculate_temp'] = _temp
            elif date.month == 6:
                _temp = datetime.date(date.year, 8, 31)
                _signal = self.factor[(self.factor['end_date'] == date) & (self.factor['f_ann_date'] <= _temp)].copy()
                _signal['calculate_temp'] = _temp
            elif date.month == 9:
                _temp = datetime.date(date.year, 10, 31)
                _signal = self.factor[(self.factor['end_date'] == date) & (self.factor['f_ann_date'] <= _temp)].copy()
                _signal['calculate_temp'] = _temp
            else:
                continue

            if _temp > datetime.date.today():
                continue

            if _signal.shape[0] < 10:
                _warnings = f"报告期数据量不足，当前报告期为：{date}; 数据量大小: {_signal.shape[0]}"
                warnings.warn(_warnings, UserWarning)
                if _signal.shape[0] == 0:
                    continue

            # 记录分组信息, 从小到大进行排序
            factor_series = _signal['signal']
            valid_data = factor_series.dropna()
            try:
                _signal.loc[valid_data.index, 'group'] = pd.qcut(
                    valid_data, self.group, labels=np.arange(1, self.group + 1),  # type: ignore
                    duplicates='drop'
                )  # type: ignore
            except Exception as e:
                print(f"分组异常，有效样本数{len(factor_series)}, 唯一值数据{_signal['signal'].unique()}, 日期：{date}")
                print(f"报错：{e}")
                try:
                    ranked = valid_data.rank(pct=True)
                    bins = pd.cut(ranked, self.group, labels=np.arange(1,
                                                                       self.group + 1))  # pyright: ignore[reportArgumentType, reportCallIssue]
                    _signal.loc[valid_data.index, 'group'] = bins
                    print(f"{date}, 采用rank+cut方式进行分组")
                except Exception as e:
                    raise ValueError(f"依旧分组失败，报错{e}")

            _holds = _signal[['code', 'group', 'calculate_temp', 'signal', 'industry_code']].copy()
            result.append(_holds)

        # 获取下一交易日
        if len(result) == 0:
            print(f"因子{signal_name}没有持仓数据")
            return

        holding = pd.concat(result)
        holding.reset_index(inplace=True, drop=True)
        holding['trading'] = self._optimized_get_next_tradingDates(holding)

        # 提取行情数据
        _start = holding['trading'].min().strftime("%Y-%m-%d")
        _end = holding['trading'].max().strftime("%Y-%m-%d")
        price = get_price(code=list(holding['code'].unique()), startdate=_start, enddate=_end)
        price = price.drop_duplicates(subset=['trade_date', 'code'], keep='last')  # 行情表数据存在重复
        # 将 price 透视成 MultiIndex 方便快速索引
        price_idx = price.set_index(['trade_date', 'code'])['open']

        # 生成调仓周期
        trading_dates = holding['trading'].unique()
        periods = pd.DataFrame({
            'start': trading_dates[:-1],
            'end': trading_dates[1:]
        })

        # ====== 批量构建买卖价格 ======
        # 扩展 holding，加上下一期日期
        holding_ext = holding.merge(periods, left_on='trading', right_on='start', how='left')

        # 查买入价和卖出价
        holding_ext['buy_open'] = price_idx.reindex(
            list(zip(holding_ext['start'], holding_ext['code']))
        ).values
        holding_ext['sell_open'] = price_idx.reindex(
            list(zip(holding_ext['end'], holding_ext['code']))
        ).values

        # 收益率
        holding_ext['ret'] = holding_ext['sell_open'] / holding_ext['buy_open'] - 1

        # 去掉缺失
        holding_ext = holding_ext.dropna(subset=['ret'])

        # ====== 按 group 和 start 汇总 ======
        group_return = (
            holding_ext.groupby(['group', 'start'], observed=False)['ret']
            .mean()
            .unstack('group')
            .to_dict()
        )
        # 计算因子IC
        self.ic = holding_ext.groupby('start').apply(lambda x: x['signal'].corr(x['ret'], method='spearman'),
                                                     include_groups=False)
        self.ic.name = 'ic_series'

        # 记录分组回测结果
        groupReturn = pd.DataFrame.from_dict(group_return, orient='columns')
        self.holding = holding
        self.group_return = groupReturn

    def _optimized_get_next_tradingDates(self, holding, n=1):
        # 获取唯一日期值
        unique_dates = holding['calculate_temp'].unique()

        # 为唯一日期计算对应的交易日
        date_mapping = {}
        for date in unique_dates:
            date_mapping[date] = get_next_tradeDate(date, self.trade_date, n=n)

        # 映射回原数据
        return holding['calculate_temp'].map(date_mapping)

    def plotting_group_return(self, signal_name="signal", is_save=False):
        """输出分组累计收益率曲线"""
        # 获取分组收益率数据
        if self.group_return is None:
            print(f"因子{signal_name}无回测数据")
            return
        _data = self.group_return.copy()

        fig, ax = plt.subplots(figsize=(8, 4))
        plt.rcParams['font.sans-serif'] = ['SimHei', 'Microsoft YaHei', 'Arial Unicode MS']
        plt.rcParams['axes.unicode_minus'] = False  # 解决负号显示问题
        for group in self.group_return.columns:
            _group = (1 + _data[group]).cumprod() - 1
            # 添加曲线
            ax.plot(_data.index, _group, label=f"group_{group}")

        ax.set_title(f"{signal_name}: accumulated_return_grouped, rebalance_period=end_dates")
        plt.figtext(0.25, 0.01,
                    "Group according to factor values from small to large")
        ax.set_xlabel("trade_date")
        ax.set_ylabel("cumulative return")
        ax.legend(loc='best')

        if is_save:
            # 使用 pathlib 处理路径，更加可靠
            save_dir = Path(r"E:\utilities\beforeTrading\backtestingresults")

            # 确保目录存在
            save_dir.mkdir(parents=True, exist_ok=True)

            # 处理文件名，避免特殊字符问题
            filename = f"{signal_name}_accumulated_return_grouped.png"
            save_path = save_dir / filename

            # 保存文件
            plt.savefig(save_path, bbox_inches='tight', dpi=300)
            print(f"因子{signal_name}-财务因子-分组回测结果已保存")

        else:
            plt.show()

    def plotting_ic(self, signal_name="signal", ma_window=5, is_save=False):

        if self.ic is None:
            print(f"因子{signal_name}没有IC数据，请先运行group_report方法进行因子检测")
            return

        # 计算IC移动平均值
        ic_ma = self.ic.rolling(window=ma_window, min_periods=1).mean()

        # 计算IC累计值
        ic_cumsum = self.ic.cumsum()

        # 计算IR值
        ir = self.ic.mean() / self.ic.std()

        # 创建图表
        fig, ax1 = plt.subplots(figsize=(8, 4))
        plt.rcParams['font.sans-serif'] = ['SimHei', 'Microsoft YaHei', 'Arial Unicode MS']
        plt.rcParams['axes.unicode_minus'] = False

        # 绘制IC值
        ax1.plot(self.ic.index, self.ic.values, color='blue', alpha=0.5, label='IC')

        # 绘制IC移动平均值
        ax1.plot(self.ic.index, ic_ma.values, color='red', linewidth=2, label=f'IC MA({ma_window})')

        # 设置第一个Y轴标签
        ax1.set_xlabel('Date')
        ax1.set_ylabel('IC & IC MA', color='blue')
        ax1.tick_params(axis='y', labelcolor='blue')

        # 创建第二个Y轴用于绘制IC累计值
        ax2 = ax1.twinx()
        ax2.plot(self.ic.index, ic_cumsum.values, color='green', linewidth=2, label='IC Cumulative')
        ax2.set_ylabel('IC Cumulative', color='green')
        ax2.tick_params(axis='y', labelcolor='green')

        # 添加图例
        lines1, labels1 = ax1.get_legend_handles_labels()
        lines2, labels2 = ax2.get_legend_handles_labels()
        ax1.legend(lines1 + lines2, labels1 + labels2, loc='upper left')

        # 添加标题和网格
        plt.title(f'{signal_name}: IC Analysis')
        ax1.grid(True, alpha=0.3)
        plt.figtext(0.5, 0.01,
                    f"Mean of IC: {self.ic.mean(): .4f}, Positive proportion: {len(self.ic[self.ic > 0]) / len(self.ic): .4f}\n"
                    f"IR: {ir: .4f}",
                    ha='center', fontsize=10, bbox={"facecolor": "orange", "alpha": 0.5, "pad": 5})

        # 保存或显示图表
        if is_save:
            save_dir = Path(r"E:\utilities\beforeTrading\backtestingresults")
            save_dir.mkdir(parents=True, exist_ok=True)
            filename = f"{signal_name}_ic_analysis.png"
            save_path = save_dir / filename
            plt.savefig(save_path, bbox_inches='tight', dpi=300)
            print(f"因子{signal_name}-财务因子-IC分析结果已保存")
        else:
            plt.show()
